#! /usr/bin/env commonjs
//
// ***** BEGIN LICENSE BLOCK *****
// Version: MPL 1.1/GPL 2.0/LGPL 2.1
//
// The contents of this file are subject to the Mozilla Public License Version
// 1.1 (the "License"); you may not use this file except in compliance with
// the License. You may obtain a copy of the License at
// http://www.mozilla.org/MPL/
//
// Software distributed under the License is distributed on an "AS IS" basis,
// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
// for the specific language governing rights and limitations under the
// License.
//
// The Initial Developer of the Original Code is PageMail, Inc.
//
// Portions created by the Initial Developer are 
// Copyright (c) 2007-2009, PageMail, Inc. All Rights Reserved.
//
// Contributor(s):
// 
// Alternatively, the contents of this file may be used under the terms of
// either of the GNU General Public License Version 2 or later (the "GPL"),
// or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
// in which case the provisions of the GPL or the LGPL are applicable instead
// of those above. If you wish to allow use of your version of this file only
// under the terms of either the GPL or the LGPL, and not to allow others to
// use your version of this file under the terms of the MPL, indicate your
// decision by deleting the provisions above and replace them with the notice
// and other provisions required by the GPL or the LGPL. If you do not delete
// the provisions above, a recipient may use your version of this file under
// the terms of any one of the MPL, the GPL or the LGPL.
//
// ***** END LICENSE BLOCK ***** 
//
// gpsee: dd
//
// @file	jazzdoc.js	JazzDoc documentation generation engine
// @author	Donny Viszneki, PageMail, Inc., hdon@page.ca
// @date	Sep 2009
//

var {ParentError} = require("util");
var {ByteString} = require("binary");
var {subst} = require("util");
const ffi = require("gffi");
const _open = new ffi.CFunction(ffi.int, "open", ffi.pointer, ffi.int);
const _creat = new ffi.CFunction(ffi.int, "creat", ffi.pointer, ffi.int);
const _close = new ffi.CFunction(ffi.int, "close", ffi.int);
const _read = new ffi.CFunction(ffi.ssize_t, "read", ffi.int, ffi.pointer, ffi.size_t);
const _write = new ffi.CFunction(ffi.ssize_t, "write", ffi.int, ffi.pointer, ffi.size_t);
/* The Error pretty() instance method produces a beautiful, readable error message */
Error.prototype.pretty =
(function()function Error_pretty() {
  var result = '';
  this.stack.replace(/^([^(]*)\(([^)]*)\)@([^:]*):([0-9]*)/gm,function(all,name,args,src,line) {
    if (result) result += '\n';
    result += 'FUNCTION '+name+' at '+(src?src+':'+line:'native');
    for each(let arg in args.split(','))
      result += '\n   '+arg;
  });
  return result;
})();
/* The RegExp iter() instance method provides a clean API for iterating over lazily-computed regular expression matches */
RegExp.prototype.iter = function(){var m;while((m=this.exec.apply(this,arguments)))yield m}
/* A SystemExit error provides a clean API for exiting the program while cleaning up finally{} blocks */
const SystemExit = {'__proto__': Error.prototype, 'toString': function() 'SystemExit'};
/* Like dirname(3) */
function dirname(path) path.substr(0,path.lastIndexOf('/',path.length-2))
/* The config object provides default properties affecting program behavior, and can be overridden at the commandline with the -O option */
var config = {
  'output':         'output.html',
  get template()    resource('template.html'),
  'home':           dirname(arguments[0]),
  'title':          'Jazzdocs',
};
/* resource() returns the path necessary for Jazzdocs to access its own data files */
function resource(filename) config.home + '/' + filename

/* main() is invoked with the top-level arguments variable as its argument vector */
function main() {
  if (arguments.length < 2) {
    usage("jazzdoc.js");
  }

  var args = arguments, i;

  /* Process arguments */
option_processing:
  for (i=1; i<arguments.length; i++) {
    function nextarg() {
      if (i+1 >= args.length)
        throw new Error('Command-line option #'+i+' "'+args[i]+'" must be followed by something');
      return args[++i];
    }
    var arg = arguments[i];
    switch (arg) {
      /* Override configuration settings */
      case '-O':
        let ovrd = nextarg();
        try { ovrd = eval('({"__proto__":config, '+ovrd+'})') }
        catch (e) { throw new Error("Syntax error in argument "+i+" that argument follows:\n"+ovrd) }
        config = ovrd;
        break;
      /* Explicitly end options section of command-line argument vector */
      case '--':
        i++;
      /* Implicitly end options section of command-line argument vector */
      default:
        if (arg != '--' && arg[0] == '-')
          print('Warning: no such option "'+arg+'"');
        break option_processing;
    }
  }

  /* Now we begin our real processing. The following variables are used during this phase. */
  var all_records = [];
  var records_by_name = {};

  /* Process filenames */
  for (i=i; i<arguments.length; i++) {
    var filename = arguments[i];
    /* Each comment containing the @jazzdoc directive, discounting comment contents preceding the @jazzdoc directive */
    for (var comment in TrimComments(StartingWith('@jazzdoc',Comments(new File(filename))))) {
      /* Create a new record */
      var record = {forms:[]};
      /* Each directive in the comment */
      for (let dir in /@(\S+)([^@]*)/g.iter(comment)) {
        switch (dir[1]) {
          case 'jazzdoc':
            record.name = dir[2].split('\n',1)[0].trim();
            record.description = wiki(dir[2].substr(dir[2].indexOf('\n')));
            records_by_name[record.name] = record;
            break;
          case 'form':
            var pos = dir[2].indexOf('\n');
            var name = dir[2].substr(0, pos);
            var full = wiki(dir[2].substr(pos));
            record.forms.push({'name': name, 'full': full});
        }
      }
      all_records.push(record);
    }
  }

  /* Okay, we have successfully parsed out a bunch of doc data! Now we can begin creating our output! */
  new Outfile(config.output).write(
    compose(File.Contents(config.template), {
      body: "whatever",
      dojo: [resource('dojo.js'), resource('dojo-hash.js')].map(File.Contents).join('\n'),
      data: '\nvar all_records = ' + uneval(records_by_name),
      title: config.title,
    })
  );

  /* That's all, folks! */
}

/* Wiki-formats the text */
function wiki(text) {
  var result = '';
  /* Entitize some characters */
  text = text.replace(/[<>&]/g, function(c){return({'<':'&lt;','>':'&gt;','&':'&amp;'})[c]});
  /* Different levels of indentation mean different kinds of formatting */
  for (var {cols, rows} in IndPlusLines(text)) {
    /* Certain codes for the first line of a paragraph change the way it's handled */
    if (rows[0].trim() == 'code:') {
      result += '<code>';
      result += rows.slice(1).join('<br/>');
      result += '</code>';
    }
    /* A colon in each line becomes a table */
    else if (rows.every(function(line){return line.indexOf(':') >= 0})) {
      result += '<table>';
      for each(var row in rows) {
        result += '<tr>';
        for each(var col in row.split(/\s*:\s*/)) {
          result += '<td>' + col + '</td>';
        }
        result += '</tr>';
      }
      result += '</table>';
    }
    /* A dash at the beginning of each line becomes a list */
    else if (rows.every(function(line){return line.trim().indexOf('-') == 0})) {
      print('found a list');
      result += '<ul>';
      for each(var row in rows)
        result += '<li>'+row.substr(1)+'</li>';
      result += '</ul>';
    }
    /* No special formatting rule? Just make it a paragraph */
    else {
      result += '<p>'+rows.join('\n')+'</p>';
    }
  }
  /* Find symbol names and linkify them. This is a very cheap method, only looking for dots in the
   * name. The runtime environment for the documentation is pretty permissive about exact names,
   * so a mention of "foo.bar" can be reasonably expected to link to "baz.foo.bar" or "foo.bar.baz"
   * for that matter. TODO In the future I'd like two passes, giving the user the opportunity to
   * see what links are ambiguous or unresolved, and maybe deal with them with an explicit index of
   * aliases and/or symbol name expansions. */
  result = result.replace(/\b[A-Za-z_$.]+\b/g, function(word) {
    var pos = word.indexOf('.');
    if (pos >= 0 && pos+1 < word.length)
      word = '<a href="#'+word+'">'+word+'</a>';
    return word;
  });
  /* */
  /* Break up into paragraphs */
  return result.replace(/\n{2,}/, '</p><p>');
}

function compose(template, scope) {
  var result = '';
  while (template) {
    var pos1 = template.indexOf('{{');
    var pos2 = template.indexOf('}}', pos1+2);
    if (pos1 < 0 || pos2 < 0)
      break;
    result += template.substr(0, pos1);
    var sub = template.substring(pos1+2, pos2);
    var subval = scope[sub];
    result += 'function' === typeof subval ? subval() : subval;
    template = template.substr(pos2+2);
  }
  return result + template;
}

/* elipsize(string, length) */
function elipsize(s, l) {
  return s.substring(0, Math.floor((l-3)/2)) + "..." + s.substring(s.length-Math.floor((l-3)/2), s.length);
}

function Outfile(path) {
  this.fd = _creat.call(path, 0664);
  if (this.fd < 0)
    throw new Error('failed opening file "'+path+'"');
  this.fd.finalizeWith(_close, this.fd);
}
Outfile.prototype.write = function() {
  for (var i=0, l=arguments.length; i<l; i++) {
    var data = String(arguments[i]);
    _write.call(this.fd, data, data.length);
  }
}

function File(path) {
  var fd = _open.call(path, ffi.std.O_RDONLY);
  if (fd < 0)
    throw new Error('failed opening file "'+path+'"');
  fd.finalizeWith(_close, fd);

  this.__iterator__ = function() {
    var r, buf = new ffi.Memory(1024);
    do {
      /* Extend our buffer with another chunk of data */
      r = _read.call(fd, buf, 1024);
      if (r > 0)
        yield buf.asString(r)
    } while (r > 0);
    if (r < 0)
      throw new Error("read(2) error!");
  }

  /* Using closures for some instance methods for the Rubyism alone! */
  var self = this;
  this.Lines = {__iterator__: function(){ for(var each in Lines(self)) yield each }};
}
/* TODO improve! */
File.Contents = function(path) {
  var buf = '';
  for (var each in new File(path))
    buf += each;
  return buf;
}

function Lines(src) {
  var buf = '';
  if ('string' === typeof src)
    src = [src];
  for each(var each in src) {
    /* Extend our buffer */
    buf += each;
    /* Yield as many lines as we have in our buffer */
    var lineEnd;
    do {
      lineEnd = buf.indexOf('\n');
      if (lineEnd >= 0) {
        var line = buf.substr(0, lineEnd);
        buf = buf.substr(lineEnd+1);
        yield line;
      }
    } while (lineEnd >= 0);
  }
  yield buf;
}

function Comments(src) {
  var buf = '', lpos, rpos;
  for (var each in src) {
    /* Extend our buffer */
    buf += each;
    /* Yield comments */
    do {
      lpos = buf.indexOf('/\*');
      if (lpos >= 0) {
        rpos = buf.indexOf('*\/', lpos+2);
        if (rpos > lpos) {
          yield buf.substring(lpos+2, rpos);
          buf = buf.substr(rpos+2);
        }
      }
    } while (lpos >= 0 && rpos > lpos)
    /* Trim buffer */
    if (lpos == -1) buf = '';
  }
  if (buf)
    throw new Error("Unterminated comment: "+ elipsize(buf.replace(/\n/g,'\\n'), 100));
}

function TrimComments(src) {
  for (var comment in src) {
    yield comment.split('\n').map(function(line){return line.replace(/^\s*\* ?/, '')}).join('\n');
  }
}

function Trim(src) {
  for (var each in src) {
    yield each.trim();
  }
}

function Contains(needle, src) {
  for (var s in src)
    if (s.indexOf(needle) >= 0)
      yield s;
}

function StartingWith(needle, src) {
  for (var s in src) {
    var pos = s.indexOf(needle);
    if (pos >= 0)
      yield s.substr(pos);
  }
}

/* Strips indentation from text, yielding an object with two properties describing the level of indentation and an
 * array of strings for each consecutive line with the same indentation level. TABS ARE NOT SAFE. */
function Indentation(src) {
  var level = -1;
  var lines;
  for (var line in Lines(src)) {
    var newlevel = line.match(/^ */)[0].length;
    if (level != newlevel) {
      if (level != -1) {
        yield {'cols': level, 'rows': lines};
      }
      level = newlevel;
      lines = [];
    }
    lines.push(line.substr(level));
  }
  yield {'cols': level, 'rows': lines};
}

/* Like Indentation() but when yielding lines, also fold multiple consecutive linefeeds into yield breaks */
function IndPlusLines(src) {
  for (var {cols,rows} in Indentation(src)) {
    var last = 0;
    for (var i=0, l=rows.length; i<l; i++) {
      if (!rows[i].trim()) {
        if (last+1<i)
          yield {'cols': cols, 'rows': rows.slice(last, i+1)};
        last = i;
      }
    }
    if (last+1<i)
      yield {'cols': cols, 'rows': rows.slice(last, i+1)};
  }
}

/* Convert some characters to HTML entities */
var entities = {'<':'lt','>':'gt','&':'amp','"':'quot',"'":'apos'}
function entitize(text) {
  return text.replace(/[<>&"']/g, function(c){return '&'+(entities[c]||('#'+c.charCodeAt(0).toString(16)))+';'});
}

function usage(argvZero) {
  print(
      "JazzDoc - Documentation Generator for CommonJS modules\n" +
      "Copyright (c) 2009 PageMail, Inc. All Rights Reserved.\n" +
      "\n" +
      "Usage: " + argvZero + " [config_object] <file> [file...]\n" +
      "Where: config_object is a valid JavaScript object whose members override the\n" +
      "                     contents of the global config variable\n" +
      "       file          is an input file\n" +
      "\n" +
      "Configuration Properties:\n" +
      "       home          Where to find JazzDoc's resources (default: " + config.home + ")\n" +
      "       output        The name of the output file (default: " + config.output + ")\n" +
      "       template      JazzDoc HTML template file (default: " + config.template + ")\n"
      );
  throw SystemExit;
}

try { main.apply(this, arguments) }
catch (e if e === SystemExit) {}
